﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Shell;
using VSLangProj80;
using System.Diagnostics;
using System.IO;
using TFDP.Common;
using Microsoft.VisualStudio;
using EnvDTE;
using System.Data;
using TFDP.VisualStudioInterop;

namespace TFDP.VsPackage.Support.TargetProjects
{
    internal sealed class VsLangProject : ITargetProject
    {
        private IVsHierarchy projectHierarchy;
        private IVsQueryEditQuerySave2 queryEditService;
        private VSProject2 project;

        #region ITargetProject Members

        public void Initialize(Microsoft.VisualStudio.Shell.Interop.IVsHierarchy projectHierarchy)
        {
            this.projectHierarchy = projectHierarchy;
            this.project = VsInteropUtilities.GetVsLangProject((IVsProject)projectHierarchy);

            EnvDTE.Project targetProject = VsInteropUtilities.GetEnvDTEProject(projectHierarchy);

            IServiceProvider serviceProvider = new ServiceProvider(
                (Microsoft.VisualStudio.OLE.Interop.IServiceProvider)targetProject.DTE
            );

            queryEditService = (IVsQueryEditQuerySave2)serviceProvider.GetService(typeof(SVsQueryEditQuerySave));
        }

        public bool Update(Common.GeneratedArtifact artifact)
        {
            if (artifact == null)
                throw new ArgumentNullException("artifact");

            string projectDirectory = Path.GetDirectoryName(VsInteropUtilities.GetFileNameFromHierarchy(projectHierarchy));
            string targetAbsolutePath = Path.Combine(projectDirectory, artifact.TargetFileName);
            string targetRelativePath = Path.GetDirectoryName(artifact.TargetFileName).Replace(projectDirectory, String.Empty);
            
            // Split the relative path into directories
            string[] pathParts = targetRelativePath.Split(
                new char[] { Path.DirectorySeparatorChar }, 
                StringSplitOptions.RemoveEmptyEntries
            );
            
            // Check if the target folders exist, starting at the root of the project
            EnvDTE.ProjectItem targetFolder = project.Project.ProjectItems.Item(pathParts[0]) as EnvDTE.ProjectItem;
            
            // Index of ProjectItems starts at 1 not 0
            for (int i = 1; i < pathParts.Length; i++)
            {
                targetFolder = targetFolder.ProjectItems.Item(pathParts[i]);

                if (targetFolder == null)
                {
                    Logger.LogError(
                        String.Format(
                            "Part of the path {0} does not exist. Missing directory {1}",
                            targetRelativePath,
                            pathParts[i]
                        )
                    );

                    return false;
                }
            }

            // Check if the file exists
            bool targetExists = false;
            bool generateIfNotExists = false;
            ProjectItem targetItem = null;

            if (targetFolder.ProjectItems.Count > 0)
            {
                var matches = targetFolder.ProjectItems.OfType<EnvDTE.ProjectItem>()
                    .Where(p => p.Name == Path.GetFileName(artifact.TargetFileName))
                    .Select(p => p);

                if (matches.Count() > 0)
                {
                    targetItem = matches.First();
                }

                targetExists = (targetItem != null);
            }
            else
                targetExists = false;

            // Some templates genrated output that should only be
            // created if the target file does not exist.
            if (artifact.Properties.ContainsKey("GenerateIfNotExists"))
                generateIfNotExists = true;

            // Check if the files are equal
            bool filesAreEqual = false;

            if (targetExists)
            {
                filesAreEqual = ProjectManager.FilesAreEqual(artifact.TemporaryFileName, targetAbsolutePath);
            }

            if (targetExists && !generateIfNotExists && !filesAreEqual)
            {
                FileEditRequestResult result = queryEditService.CanEdit(targetAbsolutePath);
                if (!result.Success)
                {
                    Logger.LogExtended(TraceLevel.Error, String.Format("Unable to write file: {0}", result.Message), targetAbsolutePath, 1, 1);
                    return false;
                }

                // Check if the artifact is to be generated via the CodeModel
                if (artifact.Properties.ContainsKey("GenerateViaCodeModel") && artifact.Properties["GenerateViaCodeModel"].ToLower() == "true")
                {
                    return UpdateViaCodeModel(artifact, targetItem);
                }
                else
                {
                    // Regular edit
                    File.Copy(artifact.TemporaryFileName, targetAbsolutePath, true);
                }
            }
            else if(!targetExists)
            {
                // Copy the temporary file to the target location
                // and add it to the project at the specified 
                // folder

                File.Copy(artifact.TemporaryFileName, targetAbsolutePath, true);
                targetFolder.ProjectItems.AddFromFile(targetAbsolutePath);
            }

            return true;
        }

        private bool UpdateViaCodeModel(GeneratedArtifact artifact, EnvDTE.ProjectItem target)
        {
            if (artifact == null)
                throw new ArgumentNullException("artifact");

            if (target == null)
                throw new ArgumentNullException("target");

            string fileName = Path.GetFileName(artifact.TargetFileName);
            string artifactName = VsLangProject.GetElementName(artifact);
            
            if (!target.IsOpen)
                target.Open();

            if (target.FileCodeModel != null)
            {
                var targetNamespace = target.FileCodeModel.CodeElements.OfType<EnvDTE.CodeElement>()
                    .ToList<EnvDTE.CodeElement>()
                    .Where(c => c.Kind == EnvDTE.vsCMElement.vsCMElementNamespace)
                    .Select(c => c);

                if (targetNamespace.Count() == 0)
                    return false;

                var targetClass = targetNamespace
                    .First()
                    .Children.OfType<EnvDTE.CodeElement>()
                    .Where(c => c.Kind == EnvDTE.vsCMElement.vsCMElementClass)
                    .Select(c => c);

                if (targetClass.Count() == 0)
                    return false;

                var targetMethod = targetClass
                    .First()
                    .Children.OfType<EnvDTE.CodeElement>()
                    .Where(m => m.Kind == EnvDTE.vsCMElement.vsCMElementFunction && m.Name == artifactName)
                    .Select(m => m);

                if (targetMethod.Count() == 0)
                {
                    CodeClass tc = (CodeClass)targetClass.First();

                    CodeFunction func = tc.AddFunction(
                        artifactName,
                        vsCMFunction.vsCMFunctionFunction,
                        vsCMTypeRef.vsCMTypeRefVoid,
                        -1
                    );

                    TextPoint startPoint = func.StartPoint.CreateEditPoint();
                    TextPoint endPoint = func.EndPoint.CreateEditPoint();
                    EditPoint editPoint = startPoint.CreateEditPoint();

                    editPoint.ReplaceText(endPoint, File.ReadAllText(artifact.TemporaryFileName).TrimEnd(), (int)(vsEPReplaceTextOptions.vsEPReplaceTextTabsSpaces | vsEPReplaceTextOptions.vsEPReplaceTextAutoformat));

                    // Save the document
                    target.Save();

                    return true;
                }
                else
                {
                    var method = targetMethod.First();
                    EditPoint ep = method.StartPoint.CreateEditPoint();
                    EditPoint epEnd = method.EndPoint.CreateEditPoint();

                    // Check if the method has changed
                    string tempBuff = File.ReadAllText(artifact.TemporaryFileName).TrimEnd();
                    string currentBuff = ep.GetText(epEnd);

                    bool changed = CheckMethodChanged(tempBuff, currentBuff);

                    if (changed)
                    {
                        // Replace the method contents
                        ep.ReplaceText(epEnd, File.ReadAllText(artifact.TemporaryFileName).TrimEnd(), (int)(vsEPReplaceTextOptions.vsEPReplaceTextTabsSpaces));

                        // Save the document
                        target.Save();
                    }

                    return true;
                }
            }
            else
            {
                Logger.LogError("Unable to initialize CodeModel for: " + fileName);
                return false;
            }
        }

        private bool CheckMethodChanged(string tempBuff, string currentBuff)
        {
            // As there is no simple way to do this just return true for now.
            // The problem lies with the formatting that Visual Studio does.
            // Tabs get converted to spaces or otherwise so a simple
            // string compare will always result in a changed method body.
            return true;
        }
        #endregion

        private static void DumpCodeElement(EnvDTE.CodeElement element)
        {
            Debug.WriteLine("Element type: " + element.Kind.ToString());

            if ((element.Kind == EnvDTE.vsCMElement.vsCMElementFunction) ||
                (element.Kind == EnvDTE.vsCMElement.vsCMElementClass) ||
                (element.Kind == EnvDTE.vsCMElement.vsCMElementNamespace))
            {
                Debug.WriteLine("Name: " + element.Name);
            }

            Debug.Indent();

            if (element.Children != null && element.Children.Count > 0)
            {
                element.Children.OfType<EnvDTE.CodeElement>()
                            .ToList<EnvDTE.CodeElement>()
                            .ForEach(c => VsLangProject.DumpCodeElement(c));
            }

            Debug.Unindent();
        }

        private static string GetElementName(GeneratedArtifact artifact)
        {
            if (artifact.Properties.ContainsKey("ElementName"))
            {
                return artifact.Properties["ElementName"].Replace("ap_", "").Replace("gap_", "");
            }
            else
            {
                return Path.GetFileNameWithoutExtension(artifact.TargetFileName);
            }
        }
    }
}
